import sys
import time
import datetime
from io import StringIO
import unittest
from xml.sax import saxutils

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html
# 参考地址：https://blog.csdn.net/qq_42762712/article/details/118904811
__author__ = "胡杰"
__version__ = "0.0.1"


class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):
        self.fp = fp

    def write(self, s):
        self.fp.write(s)

    def writelines(self, lines):
        self.fp.writelines(lines)

    def flush(self):
        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)

class Template_mixin(object):

    STATUS = {
        0: u'通过',
        1: u'失败',
        2: u'错误',
    }
    #对应STATUS 0,1,2
    BTN_STYLE_CLASS={
        0:'btn-success',
        1:'btn-danger',
        2:'btn-warning',
    }

    DEFAULT_TITLE = '自动化测试报告带截图'
    DEFAULT_DESCRIPTION = ''
    DEFAULT_TESTER='胡杰'

    RESULT = None
    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>

    <link href="http://libs.baidu.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
    <script src="http://libs.baidu.com//bootstrap/3.0.3/js/bootstrap.min.js"></script>
    %(stylesheet)s

</head>
<body>
    <script language="javascript" type="text/javascript">
    output_list = Array();

    /* level - 0:Summary; 1:Failed; 2:Pass 3:All 4:Error*/
    function showCase(level) {
        trs = document.getElementsByTagName("tr");
        for (var i = 0; i < trs.length; i++) {
            tr = trs[i];
            id = tr.id;
            if (id.substr(0,2) == 'ft') {
                if (level ==2 || level ==0) {
                    tr.className = 'hiddenRow';
                }
                else {
                    tr.className = '';
                }
            }
            if (id.substr(0,2) == 'pt') {
                if (level <2 || level ==4) {
                    tr.className = 'hiddenRow';
                }
                else {
                    tr.className = '';
                }
            }
        }
        //加入【详细】切换文字变化
        detail_class=document.getElementsByClassName("detail");
        //console.log(detail_class.length);
        if(level ==3){
            for(var i = 0; i < detail_class.length; i++) {
                detail_class[i].innerHTML="收起"
            }
        }
        else{
            for(var i = 0; i < detail_class.length; i++) {
                detail_class[i].innerHTML="详细"
            }
        }
    }

    function showClassDetail(cid, count) {
        var id_list = Array(count);
        var toHide = 1;
        for (var i = 0; i < count; i++) {
        //id是.不是_，忘了在哪改的了
            tid0 = 't' + cid.substr(1) + '.' + (i+1);
            tid = 'f' + tid0;
            tr = document.getElementById(tid);
            if (!tr) {
                tid = 'p' + tid0;
                tr = document.getElementById(tid);
            }
            id_list[i] = tid;
            if (tr.className) {
                toHide = 0;
            }
        }
        for (var i = 0; i < count; i++) {
            tid = id_list[i];
            if (toHide) {
                document.getElementById(tid).className = 'hiddenRow';
                document.getElementById(cid).innerText = "详细"
            }
            else {
                document.getElementById(tid).className = '';
                document.getElementById(cid).innerText="收起"
            }
        }
    }
    
    function html_escape(s) {
        s = s.replace(/&/g,'&');
        s = s.replace(/</g,'<');
        s = s.replace(/>/g,'>');
        return s;
    }
</script>
%(heading)s
%(report)s
%(ending)s
</body>
</html>
"""  # variables: (title, generator, stylesheet, heading, report, ending)

    STYLESHEET_TMPL = """
<style type="text/css" media="screen">
    body        { font-family: Microsoft YaHei,Tahoma,arial,helvetica,sans-serif;padding:20px; font-size: 80%; }
    table       { font-size: 100%; }
    /* -- heading ---------------------------------------------------------------------- */
    .heading {
        margin-top: 0ex;
        margin-bottom: 1ex;
    }
    .heading .description {
        margin-top: 4ex;
        margin-bottom: 6ex;
    }
    /* -- report ------------------------------------------------------------------------ */
    #total_row  { font-weight: bold; }
    .passClass  { color: #5cb85c; }
    .failClass  { color: #d9534f; front-weight:bold;}
    .errorClass { color: #f0ad4e; front-weight:bold;}
    .hiddenRow  { display: none; }    
    .testcase   { margin-left: 2em; }
</style>
"""

    HEADING_TMPL = """<div class='page-header'>
        <h4 style="front-family:Microsoft YaHei">%(title)s</h4>
    %(parameters)s
    <p class='description'>%(description)s</p>
    </div>
"""  # variables: (title, parameters, description)

    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
"""  # variables: (name, value)

    REPORT_TMPL = """
        <p id='show_detail_line'>
            <a class="btn btn-primary" href='javascript:showCase(0)'>概要{%(passrate)s}</a>
            <a class="btn btn-warning" href='javascript:showCase(4)'>错误{%(error)s}</a>
            <a class="btn btn-danger" href='javascript:showCase(1)'>失败{%(fail)s}</a>
            <a class="btn btn-success" href='javascript:showCase(2)'>通过{%(Pass)s}</a>
            <a class="btn btn-info" href='javascript:showCase(3)'>所有{%(count)s}</a>
        </p>
        <table id='result_table' class="table table-condensed table-bordered table-hover">
            <colgroup>
                <col align='left' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
                <col align='right' />
            </colgroup>
            <tr id='header_row' class="text-center success" style="font-weight: bold;font-size:14px">
                <td>测试套件/测试用例</td>
                <td>总数</td>
                <td>通过</td>
                <td>失败</td>
                <td>错误</td>
                <td>详细</td>
                <td>截图</td>
            </tr>
            %(test_list)s
            <tr id='total_row' class="text-center active">
                <td>总计</td>
                <td>%(count)s</td>
                <td>%(Pass)s</td>
                <td>%(fail)s</td>
                <td>%(error)s</td>
                <td>通过率：%(passrate)s</td>
                <td><a href="" target="_blank"></a></td>
            </tr>
        </table>
    """  # variables: (test_list, count, Pass, fail, error,passrate)
    REPORT_CLASS_TMPL = r"""
    <tr class='%(style)s warning'>
        <td>%(desc)s</td>
        <td class="text-center">%(count)s</td>
        <td class="text-center">%(Pass)s</td>
        <td class="text-center">%(fail)s</td>
        <td class="text-center">%(error)s</td>
        <td class="text-center"><a href="javascript:showClassDetail('%(cid)s',%(count)s)" class="detail" id='%(cid)s'>详情</a></td>
        <td class="text-center">断言或错误截图</td>
    </tr>
"""  # variables: (style, desc, count, Pass, fail, error, cid)

    REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s' width='300px'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='left' width='600px'>
    
    <!-- 默认展开错误信息 -->
    <button id='btn_%(tid)s' type="button" class="btn %(btnclass)s btn-xs" data-toggle="collapse" data-target='#div_%(tid)s>%(status)s'</button>
    <div id='div_%(tid)s' class="collapse in">
        <pre style="overflow-y: scroll;overflow-x: hidden;height:200px;width:600px;margin:auto;border:1px solid black #eee;">
        %(script)s
        </pre>
    </div>
    <!--css div popup end-->
    </td>
    <!--这里显示图片-->
    <td align="right">
        <a>
            <img src="%(image)s" height="200px" width="400px"/>
        </a>
    </td>
</tr>
"""  # variables: (tid, Class, style, desc, status)

    REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'><span class="label label-success success">%(status)s</span></td>
    <td aling="right">
        <a>
            <img src="%(image)s" height="200px" width="400px"/>
        </a>
    </td>
</tr>
"""  # variables: (tid, Class, style, desc, status)

    REPORT_TEST_OUTPUT_TMPL = r"""%(id)s: %(output)s"""  # variables: (id, output)
    #返回顶部按钮
    ENDING_TMPL = """<div id='ending'>&nbsp;</div>
    <div style="position:fixed;right:50px;bottom:30px;width:20px;height:20px;cursor:pointer">
    <a href="#"><span class="glyphicon glyphicon-eject" style="font-size:30px;" aria-hidden="true"></span></a>
    """
TestResult = unittest.TestResult

class _TestResult(TestResult):
    # note: _TestResult is a pure representation of results.
    # It lacks the output and reporting ability compares to unittest._TextTestResult.

    def __init__(self, verbosity=1):
        TestResult.__init__(self)
        self.stdout0 = None
        self.stderr0 = None
        self.success_count = 0
        self.failure_count = 0
        self.error_count = 0
        self.verbosity = verbosity
        self.outputBuffer=StringIO()

        # result is a list of result in 4 tuple
        # (
        #   result code (0: success; 1: fail; 2: error),
        #   TestCase object,
        #   Test output (byte string),
        #   stack trace,
        # )
        self.result = []
        self.passrate=float(0)

    def startTest(self, test):
        TestResult.startTest(self, test)
        # just one buffer for both stdout and stderr
        self.outputBuffer = StringIO()
        stdout_redirector.fp = self.outputBuffer
        stderr_redirector.fp = self.outputBuffer
        self.stdout0 = sys.stdout
        self.stderr0 = sys.stderr
        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector

    def complete_output(self):
        """
        Disconnect output redirection and return buffer.
        Safe to call multiple times.
        """
        if self.stdout0:
            sys.stdout = self.stdout0
            sys.stderr = self.stderr0
            self.stdout0 = None
            self.stderr0 = None
        return self.outputBuffer.getvalue()

    def stopTest(self, test):
        # Usually one of addSuccess, addError or addFailure would have been called.
        # But there are some path in unittest that would bypass this.
        # We must disconnect stdout in stopTest(), which is guaranteed to be called.
        self.complete_output()

    def addSuccess(self, test):
            self.success_count += 1
            TestResult.addSuccess(self, test)
            output = self.complete_output()
            self.result.append((0, test, output, ''))
            if self.verbosity > 1:
                sys.stderr.write('ok ')
                sys.stderr.write(str(test))
                sys.stderr.write('\n')
            else:
                sys.stderr.write('.')

    def addError(self, test, err):
        self.error_count += 1
        TestResult.addError(self, test, err)
        _, _exc_str = self.errors[-1]
        output = self.complete_output()
        self.result.append((2, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('E  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('E')

    def addFailure(self, test, err):
        self.failure_count += 1
        TestResult.addFailure(self, test, err)
        _, _exc_str = self.failures[-1]
        output = self.complete_output()
        self.result.append((1, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('F  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('F')

class HTMLTestRunner(Template_mixin):

    def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None,tester=None):
        self.stream = stream
        self.verbosity = verbosity
        if title is None:
            self.title = self.DEFAULT_TITLE
        else:
            self.title = title
        if description is None:
            self.description = self.DEFAULT_DESCRIPTION
        else:
            self.description = description
        if tester is None:
            self.tester = self.DEFAULT_TESTER
        else:
            self.tester = tester
        self.startTime = datetime.datetime.now()

    def run(self, test):
        "Run the given test case or test suite."
        result = _TestResult(self.verbosity)
        test(result)
        self.stopTime = datetime.datetime.now()
        self.generateReport(test, result)
        print(sys.stderr,'\nTime Elapsed: %s' % (self.stopTime-self.startTime))
        return result

    def sortResult(self, result_list):
        # unittest does not seems to run in any particular order.
        # Here at least we want to group them together by class.
        rmap = {}
        classes = []
        for n,t,o,e in result_list:
            cls = t.__class__
            if not cls in rmap:
                rmap[cls] = []
                classes.append(cls)
            rmap[cls].append((n,t,o,e))
        r = [(cls, rmap[cls]) for cls in classes]
        return r

    def getReportAttributes(self, result):
        """
        Return report attributes as a list of (name, value).
        Override this to add custom attributes.
        """
        startTime = str(self.startTime)[:19]
        duration = str(self.stopTime - self.startTime)
        status = []
        status.append('共%s个用例'%(result.success_count+result.failure_count+result.error_count))
        if result.success_count: status.append(u'通过 %s' % result.success_count)
        if result.failure_count: status.append(u'失败 %s' % result.failure_count)
        if result.error_count:   status.append(u'错误 %s' % result.error_count  )
        if status:
            status = ','.join(status)
            self.passrate=str("%.2f%%" % (float(result.success_count)/float(result.success_count+result.failure_count+result.error_count)*100))
            self.RESULT='{},通过率={}'.format(status,self.passrate)
        else:
            status = 'none'
        return [
            (u'测试人员',self.tester),
            (u'开始时间', startTime),
            (u'运行时长', duration),
            (u'测试结果', self.RESULT),
        ]

    def generateReport(self, test, result):
        report_attrs = self.getReportAttributes(result)
        generator = 'HTMLTestRunner %s' % __version__
        stylesheet = self._generate_stylesheet()
        heading = self._generate_heading(report_attrs)
        report = self._generate_report(result)
        ending = self._generate_ending()
        output = self.HTML_TMPL % dict(
            title = saxutils.escape(self.title),
            generator = generator,
            stylesheet = stylesheet,
            heading = heading,
            report = report,
            ending = ending,
        )
        #在 Python 3 中，write() 方法需要字符串（str）而不是字节（bytes）
        self.stream.write(output.encode('utf-8'))
        # self.stream.write(output)

    def _generate_stylesheet(self):
        return self.STYLESHEET_TMPL

    def _generate_heading(self, report_attrs):
        a_lines = []
        for name, value in report_attrs:
            line = self.HEADING_ATTRIBUTE_TMPL % dict(
                name = saxutils.escape(name),
                value = saxutils.escape(value),
            )
            a_lines.append(line)
        heading = self.HEADING_TMPL % dict(
            title = saxutils.escape(self.title),
            parameters = ''.join(a_lines),
            description = saxutils.escape(self.description),
            tester = saxutils.escape(self.tester),
        )
        return heading

    def _generate_report(self, result):
        rows = []
        sortedResult = self.sortResult(result.result)
        for cid, (cls, cls_results) in enumerate(sortedResult):
            # subtotal for a class
            np = nf = ne = 0
            for n,t,o,e in cls_results:
                if n == 0: np += 1
                elif n == 1: nf += 1
                else: ne += 1

            # format class description
            if cls.__module__ == "__main__":
                name = cls.__name__
            else:
                name = "%s.%s" % (cls.__module__, cls.__name__)
            doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
            desc = doc and '%s: %s' % (name, doc) or name

            row = self.REPORT_CLASS_TMPL % dict(
                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                desc = desc,
                count = np+nf+ne,
                Pass = np,
                fail = nf,
                error = ne,
                cid = 'c%s' % (cid+1),
            )
            rows.append(row)

            for tid, (n,t,o,e) in enumerate(cls_results):
                self._generate_report_test(rows, cid, tid, n, t, o, e)

        report = self.REPORT_TMPL % dict(
            test_list = ''.join(rows),
            count = str(result.success_count+result.failure_count+result.error_count),
            Pass = str(result.success_count),
            fail = str(result.failure_count),
            error = str(result.error_count),
            passrate = self.passrate,
        )

        return report

    def _generate_report_test(self, rows, cid, tid, n, t, o, e):
        # e.g. 'pt1.1', 'ft1.1', etc
        has_output = bool(o or e)
        #这里设置了id 1.1
        tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
        name = t.id().split('.')[-1]
        doc = t.shortDescription() or ""
        desc = doc and ('%s: %s' % (name, doc)) or name
        tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL

        if isinstance(o,str):
            uo=o
        else:
            uo=o

        if isinstance(e,str):
            ue=e
        else:
            ue=e

        script = self.REPORT_TEST_OUTPUT_TMPL % dict(
            id=tid,
            output=saxutils.escape(uo+ue),
        )
        #插入图片
        start_key='screenshot:'
        end_key='.png'
        unum=str(uo).rfind(start_key)
        if ((uo or ue) and unum!=-1):
            hidde_status=''
            left=int(str(uo).rfind(start_key))
            right=int(str(uo).rfind(end_key))
            img_name=str(str(uo)[left+len(start_key):left+right-left+len(end_key)]).replace('','')
            image_url='../screenshots/{}'.format(img_name)
        else:
            hidde_status='''hidden="hidden"'''
            image_url='""'
            # a='C:\\code\\Python\\autoClick\\autotest\\screenshots\\20250919171404.png'

        row = tmpl % dict(
            tid=tid,
            Class=(n == 0 and 'hiddenRow' or 'none'),
            style=n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
            desc=desc,
            script=script,
            hidden=hidde_status,
            image=image_url,
            btnclass=self.BTN_STYLE_CLASS[n],
            status=self.STATUS[n],
        )
        rows.append(row)
        if not has_output:
            return

    def _generate_ending(self):
        return self.ENDING_TMPL

class TestProgram(unittest.TestProgram):
    """
    A variation of the unittest.TestProgram. Please refer to the base
    class for command line parameters.
    """
    def runTests(self):
        # Pick HTMLTestRunner as the default test runner.
        # base class's testRunner parameter is not useful because it means
        # we have to instantiate HTMLTestRunner before we know self.verbosity.
        if self.testRunner is None:
            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
        unittest.TestProgram.runTests(self)

main = TestProgram

##############################################################################
# Executing this module from the command line
##############################################################################

if __name__ == "__main__":
    main(module=None)